<?php

namespace pmvc\ioc;

use pmvc\Properties;
use pmvc\log\Log;
use pmvc\util\ClassUtil;
use pmvc\util\ReflectionUtil;

use ReflectionClass;
use Exception;

/**
 * Implementation of the {@link ObjectFactory} that uses
 * {@link ObjectDescriptor}s and {@link PropertyDescriptor}s.
 *
 */
class DescriptorObjectFactory
	implements ObjectFactory {

	private $parent					= NULL;
	private $objects				= Array();
	private $descriptorsById		= Array();
	private $descriptorsByName		= Array();
	private $descriptors			= Array();
	private $initialized			= FALSE;
	private $initializedPostProcess	= FALSE;
	private $preloadedFiles			= FALSE;

	/**
	 * Creates the ObjectFactory with the given
	 * parent.
	 * @param $parent the parent
	 */
	public function __construct(ObjectFactory $parent=NULL) {
		$this->parent = $parent;
	}

	/**
	 * Destructor.
	 */
	public function __destruct() {
		foreach ($this->descriptors as $desc) {
			if (isset($this->objects[$desc->id])) {
				foreach ($this->objects[$desc->id] as $object) {
					if (ReflectionUtil::isInstance($object, 'pmvc\ioc\DestroyingObject')) {
						$object->destroy($this);
					}
					if (!empty($desc->destroyMethod)) {
						call_user_func(array($object, $desc->destroyMethod));
					}
				}
			}
		}
	}

	/**
	 * Initializes the ObjectFactory.
	 */
	public function initialize() {
		if ($this->initialized) { return; }
		Log::trace(__CLASS__, "Initializing ObjectFactory");
		$this->initialized = TRUE;

		$this->preloadedFiles = TRUE;
		Log::trace(__CLASS__, "Preloading files");
		foreach ($this->descriptors as $desc) {
			if (!empty($desc->preloadFile) && isset($desc->preloadFile)) {
				Log::trace(__CLASS__, "Preloading {0}", $desc->preloadFile);
				ClassUtil::requireFile($desc->preloadFile, TRUE);
			}
		}

		$this->initializedPostProcess = FALSE;
		Log::trace(__CLASS__, "Initializing DescriptorPostProcessors");
		foreach ($this->getDescriptorsOfType("pmvc\ioc\DescriptorPostProcessor") as $desc) {
			$obj = $this->getObjectByDescriptor($desc);
			foreach ($this->descriptors as $desc) {
				$obj->process($desc);
			}
		}
		
		Log::trace(__CLASS__, "Initializing ObjectPostProcessors");
		foreach ($this->getDescriptorsOfType("pmvc\ioc\ObjectPostProcessor") as $desc) {
			$this->getObjectByDescriptor($desc);
		}
		$this->initializedPostProcess = TRUE;
		
		Log::trace(__CLASS__, "Initializing non lazy singletons");
		foreach ($this->descriptors as $desc) {
			if (!$desc->lazy && !$desc->abstract && $desc->singleton) {
				Log::trace(__CLASS__, "Initializing non lazy singleton: {0}", $desc->class);
				$this->getObjectByDescriptor($desc);
			}
		}
		
		Log::trace(__CLASS__, "Initializing of ObjectFactory complete");
	}

	/**
	 * Returns the serialized state of the DescriptorObjectFactory.
	 * @return the serialized state
	 */
	public function getSerializedState() {
		return serialize(array(
			"descriptorsById"		=> $this->descriptorsById,
			"descriptorsByName"		=> $this->descriptorsByName,
			"descriptors"			=> $this->descriptors
		));
	}

	/**
	 * Returns the serialized state of the DescriptorObjectFactory.
	 * @return the serialized state
	 */
	public function loadSerializedState($state) {
		$state = unserialize($state);
		$this->descriptorsById		= $state["descriptorsById"];
		$this->descriptorsByName	= $state["descriptorsByName"];
		$this->descriptors			= $state["descriptors"];
	}

	/**
	 * {@inheritDoc}
	 */
	public function getDescriptorById($id) {
		return (array_key_exists($id, $this->descriptorsById))
			? $this->descriptorsById[$id] : NULL;
	}

	/**
	 * {@inheritDoc}
	 */
	public function getDescriptorByName($name) {
		return (array_key_exists($name, $this->descriptorsByName))
			? $this->descriptorsByName[$name] : NULL;
	}

	/**
	 * {@inheritDoc}
	 */
	public function getDescriptorsOfType($type) {
		$ret = Array();
		foreach ($this->descriptors as $desc) {
			if (!$desc->abstract && ReflectionUtil::classIsA($desc->class, $type)) {
				array_push($ret, $desc);
			}
		}
		return $ret;
	}
	
	/**
	 * {@inheritDoc}
	 */
	public function addObjectDescriptor(ObjectDescriptor $desc) {

		// validate it
		if (!empty($desc->id) 
			&& array_key_exists($desc->id, $this->descriptorsById)) {
			throw new Exception(
				"ObjectDescriptor with id ".$desc->id." exists in this ObjectFactory");
		} else if (empty($desc->id)) {
			throw new Exception(
				"ObjectDescriptor with type ".$desc->class." does not have an id");
		} else if (!empty($desc->name)
			&& array_key_exists($desc->name, $this->descriptorsByName)) {
			throw new Exception(
				"ObjectDescriptor with name ".$desc->name." exists in this ObjectFactory");
		} else if (!$desc->abstract && empty($desc->class)) {
			throw new Exception(
				"Non abstract ObjectDescriptors must have a class");
		}
		
		// add it
		if (!empty($desc->id)) { $this->descriptorsById[$desc->id] = $desc; }
		if (!empty($desc->name)) { $this->descriptorsByName[$desc->name] = $desc; }
		array_push($this->descriptors, $desc);
		Log::trace(__CLASS__, "Added descriptor for: {0}", $desc->class);
		if ($this->initialized) {
			$this->initializeDescriptor($desc);
		}
	}

	/**
	 * {@inheritDoc}
	 */
	public function getObjectsOfType($type, $descIgnore=NULL) {
		$descriptors = $this->getDescriptorsOfType($type);
		$ret = Array();
		foreach ($descriptors as $desc) {
			if ($desc!=$descIgnore) {
				array_push($ret, $this->getObjectByDescriptor($desc));
			}
		}
		OrderUtil::sort($ret);
		return $ret;
	}

	/**
	 * {@inheritDoc}
	 */
	public function getObjectById($id) {
		if (!is_string($id)) {
			throw new Exception("Object ID must be a string");
		}
		if (!array_key_exists($id, $this->descriptorsById) && empty($this->parent)) {
			throw new Exception(
				"Unable to find object with id:".$id);
		} else if (!array_key_exists($id, $this->descriptorsById) && !empty($this->parent)) {
			return $this->parent->getObjectById($id);
		}
		return $this->getObjectByDescriptor($this->descriptorsById[$id]);
	}

	/**
	 * {@inheritDoc}
	 */
	public function getObjectByName($name) {
		if (!array_key_exists($name, $this->descriptorsByName) && empty($this->parent)) {
			throw new Exception(
				"Unable to find object with name: ".$name);
		} else if (!array_key_exists($name, $this->descriptorsByName) && !empty($this->parent)) {
			return $this->parent->getObjectByName($name);
		}
		return $this->getObjectByDescriptor($this->descriptorsByName[$name]);
	}

	/**
	 * {@inheritDoc}
	 */
	public function getObjectIds() {
		return array_keys($this->descriptorsById);
	}

	/**
	 * {@inheritDoc}
	 */
	public function getObjectNames() {
		return array_keys($this->descriptorsByName);
	}

	/**
	 * {@inheritDoc}
	 */
	public function getObjectIdsOfType($type) {
		$ret = Array();
		foreach ($this->descriptorsById as $id => $desc) {
			if (ReflectionUtil::classIsA($desc->class, $type)) {
				array_push($ret, $id);
			}
		}
		return $ret;
	}

	/**
	 * {@inheritDoc}
	 */
	public function getObjectNamesOfType($type) {
		$ret = Array();
		foreach ($this->descriptorsByName as $name => $desc) {
			if (ReflectionUtil::classIsA($desc->class, $type)) {
				array_push($ret, $name);
			}
		}
		return $ret;
	}

	/**
	 * {@inheritDoc}
	 */
	public function hasObjectWithName($name, $recurse=true) {
		$ret = array_key_exists($name, $this->descriptorsByName);
		if ($ret) { 
			return TRUE; 
		} else if (!empty($this->parent)) {
			return $this->parent->hasObjectWithName($name, $recurse);
		} else {
			return FALSE;
		}
	}

	/**
	 * {@inheritDoc}
	 */
	public function hasObjectWithId($name, $recurse=true) {
		$ret = array_key_exists($name, $this->descriptorsById);
		if ($ret) { 
			return TRUE; 
		} else if (!empty($this->parent)) {
			return $this->parent->hasObjectWithId($name, $recurse);
		} else {
			return FALSE;
		}
	}
	
	/**
	 * {@inheritDoc}
	 */
	public function visitDescriptors($cb) {
		foreach ($this->descriptors as $desc) {
			$get = function()use($desc) {
				return $factory->getObjectByDescriptor($desc);
			};
			if ($cb($desc, $get)===false) {
				return;
			}
		}
	}

	/**
	 * Initializes the given descriptor.
	 * @param ObjectDescriptor $desc
	 */
	private function initializeDescriptor(ObjectDescriptor $desc) {
		if (!$this->initialized) {
			throw new Exception("initializeDescriptor may not be before the ObjectFactory is initialized");
		}
		Log::trace(__CLASS__, "Initializing ObjectDescriptor after ObjectFactoryInitialization: #".$desc->id);

		// preload
		if (!empty($desc->preloadFile) && isset($desc->preloadFile)) {
			Log::trace(__CLASS__, "Preloading {0}", $desc->preloadFile);
			ClassUtil::requireFile($desc->preloadFile, TRUE);
		}

		// DescriptorPostProcessor
		foreach ($this->getObjectsOfType("pmvc\ioc\DescriptorPostProcessor") as $obj) {
			$obj->process($desc);
		}

		// non-lazy singletons
		if (!$desc->lazy && !$desc->abstract && $desc->singleton) {
			Log::trace(__CLASS__, "Initializing non lazy singleton: {0}", $desc->class);
			$this->getObjectByDescriptor($desc);
		}
		
		Log::trace(__CLASS__, "Initializing of #".$desc->id." complete");
	}

	/**
	 * Returns the value of a given property descriptor.
	 * @param unknown_type $prop
	 */
	private function getPropertyDescriptorValue($prop) {

		// special handling for arrays
		if ($prop->type==PropertyDescriptor::TYPE_ARRAY) {
			$ret = Array();
			foreach ($prop->value as $p) {
				if (!empty($p->name)) {
					$ret[$p->name] = $this->getPropertyDescriptorValue($p);
				} else {
					array_push($ret, $this->getPropertyDescriptorValue($p));
				}
			}
			return $ret;
		}

		// all of the other types
		switch($prop->type) {
			case PropertyDescriptor::TYPE_STRING:
				return strval($prop->value);
			case PropertyDescriptor::TYPE_INTEGER:
				return intval($prop->value);
			case PropertyDescriptor::TYPE_FLOAT:
				return floatval($prop->value);
			case PropertyDescriptor::TYPE_BOOLEAN:
				return ($prop->value==1 || trim($prop->value)=="1" || strtolower(trim($prop->value))=="true");
			case PropertyDescriptor::TYPE_JSON:
				$ret = json_decode($prop->value);
				$error = json_last_error();
				if ($error!=JSON_ERROR_NONE) {
					throw new Exception("json_last_error reported error: ".$error. " for property: ".$prop->name);
				}
				return $ret;
			case PropertyDescriptor::TYPE_OBJECT:
				$ret = unserialize($prop->value);
				if ($ret===false) {
					throw new Exception("Error deserializing property: ".$prop->name);
				}
			case PropertyDescriptor::TYPE_REF:
				return $this->getObjectById($prop->value);
			default:
				throw new Exception("Invalid type for property ".$prop->name.": ".$prop->type);
		}
	}

	/**
	 * Returns all of the {@link PropertyDescriptor}s for a
	 * given {@link ObjectDescriptor}.
	 * @param $desc the {@link ObjectDescriptor}
	 * @param $propDeps the {@link PropertyDescriptor}s
	 */
	private function getPropertyDescriptorValues($desc, $propDeps=Array()) {
		if (!empty($desc->parent)) {
			$parentDesc = $this->getDescriptorByid($desc->parent);
			$propDeps = $this->getPropertyDescriptorValues($parentDesc, $propDeps);
		}
		if (!empty($desc->properties)) {
			foreach ($desc->properties as $prop) {
				$propDeps[$prop->name] = $this->getPropertyDescriptorValue($prop);
			}
		}
		return $propDeps;
	}
	
	/**
	 * Creates an object for the given descriptor.
	 * @param ObjectDescriptor $desc
	 */
	private function getObjectByDescriptor($desc) {
		if (!$this->initialized) {
			$this->initialize();
		}

		// return already created singletons
		if (isset($this->objects[$desc->id])
			&& count($this->objects[$desc->id])>0
			&& $desc->singleton) {
			return $this->objects[$desc->id][0];
		}

		// bail if it's abstract
		if ($desc->abstract) {
			throw new Exception("Can't instantiate abstract objects");
		}

		// get the property values
		Log::trace(__CLASS__, "Instantiating {0}", $desc->class);
		$propDeps = $this->getPropertyDescriptorValues($desc);

		/// get the constructor values
		$ctrDeps = Array();
		if (!empty($desc->constructorArgs)) {
			foreach ($desc->constructorArgs as $prop) {
				$ctrDeps[] = $this->getPropertyDescriptorValue($prop);
			}
		}

		// create the reflection class
		$refClass = NULL;
		$isFactory = FALSE;
		if (!empty($desc->factoryClass)) {
			$refClass =  new ReflectionClass($desc->factoryClass);
			if (!ReflectionUtil::classIsA($desc->factoryClass, 'pmvc\ioc\FactoryObject')) {
				throw new \Exception("Object {0} with factory class of type {1} does not implement FactoryObject",
				$desc->id, $desc->factoryClass);
			}
			$isFactory = TRUE;
		} else {
			$refClass = new ReflectionClass($desc->class);
		}
		

		// instantiate the object
		$object = (count($ctrDeps)>0)
			? $refClass->newInstanceArgs($ctrDeps)
			: $refClass->newInstance();

		// do *aware stuff
		if (ReflectionUtil::isInstance($object, 'pmvc\ioc\ObjectNameAware')) {
			$object->setObjectName($desc->name);
		}
		if (ReflectionUtil::isInstance($object, 'pmvc\ioc\ObjectFactoryAware')) {
			$object->setObjectFactory($this);
		}

		// populate it's properties
		foreach ($propDeps as $propName => $propValue) {
			ReflectionUtil::setProperty($object, $propName, $propValue, false, $refClass);
		}

		// the replacement object
		$replacementObject = $object;

		// call the factory if needed and initialize
		// it before the ObjectPostProcessors
		if ($isFactory) {
			if (ReflectionUtil::isInstance($object, 'pmvc\ioc\InitializingObject')) {
				$object->initialize();
			}
			$replacementObject = $replacementObject->createObject();
		}

		// call post processor
		if ($this->initializedPostProcess) {
			foreach ($this->getObjectsOfType("pmvc\ioc\ObjectPostProcessor", $desc) as $obj) {
				$ret = $obj->processBeforeInitialization($desc->name, $desc->id, $replacementObject);
				if (!empty($ret) && $ret!==FALSE) {
					$replacementObject = $ret;
				}
			}
		} else {
			Log::debug(__CLASS__, "Object '{0}' not eligible for getting processed by ObjectPostProcessors", $desc->id);
		}

		// initialize it if it's not a factory
		// and needs to be
		if (!$isFactory && ReflectionUtil::isInstance($object, 'pmvc\ioc\InitializingObject')) {
			$object->initialize();
		}

		// call init method if it needs to be
		if (!empty($desc->initMethod)) {
			call_user_func(array($object, $desc->initMethod));
		}

		// call post processor
		if ($this->initializedPostProcess) {
			foreach ($this->getObjectsOfType("pmvc\ioc\ObjectPostProcessor", $desc) as $obj) {
				$ret = $obj->processAfterInitialization($desc->name, $desc->id, $replacementObject);
				if (!empty($ret) && $ret!==FALSE) {
					$replacementObject = $ret;
				}
			}
		}

		// replace the object
		$object = $replacementObject;

		// return the object
		if (!isset($this->objects[$desc->id])) {
			$this->objects[$desc->id] = Array();
		}
		array_push($this->objects[$desc->id], $object);
		return $object;
	}
	
}

?>